#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import os
import os.path
import sys

from abc import abstractmethod

from ctools.datahelper.functree import SNode
from ctools.common.strhelper import *

import logging
from ctools.logext import *

_fpath = os.path.dirname(os.path.realpath(__file__))
_rlog, _plog, _mlog, _tlog = get_loggers(__file__, __name__, propagate=False)

class _TextParser(object):
    @abstractmethod
    def do_parse_text(self, text:str):
        return None

    @abstractmethod
    def do_parse_file(self, file:str):
        return None

class TreeTextParser(_TextParser):
    def __init__(self):
        self._level = 0
        self._curr = self._root = None

    def _check_line(self, line:str):
        if line.find('+-- ') >= 0:
            return True
        if line.find(' --+') > 0:
            return True
        return False

    def _reverse_text(self, text, prefix=''):
        lines = text.split('\n')
        lines.reverse()
        lines = [line[len(prefix):].rstrip() for line in lines]

        newlines = []
        for line in lines:
            pos = line.find(' --+')
            left = reverse_str(line[pos:])
            right = line[:pos].strip()
            newlines.append(f'{left}{right}')

        return '\n'.join(newlines)

    def _parse_line(self, line, padlen=4):
        level = line.find('+-- ') // padlen
        # function = line.split('+-- ')[1].strip()
        function = line[level * padlen + 4:].strip()
        direction = self._curr.direction
        node = SNode(function, direction)
        _mlog.debug(f"curr: {self._curr} {self._level}, node: {node} {level}")

        if level == self._level:
            self._curr.parent.add_child(node)
        elif level > self._level:
            self._level = level
            self._curr.add_child(node)
        elif level < self._level:
            while level < self._level:
                self._level -= 1
                self._curr = self._curr.parent

            self._curr.parent.add_child(node)

        self._curr = node

    def do_parse_text(self, text:str, prefix='', padlen=4) -> SNode:
        direction = '>'
        if '--+' in text:
            direction = '<'
            text = self._reverse_text(text, prefix)
        self._curr = self._root = SNode(direction=direction)
        self._level = 0
        for line in text.split('\n'):
            line = line.strip()
            if self._check_line(line):
                self._parse_line(line, padlen)

        _mlog.debug(f"len: {len(self._root)}")
        # for c in self._root:
        #     print(c)

        # drop root node if only one callee
        if len(self._root) == 1:
            return self._root[0].make_self_root()
        return self._root

    def do_parse_file(self, file:str) -> SNode:
        # TODO: direction
        self._curr = self._root = SNode(direction='>')
        self._level = 0
        with open(filename, 'r') as fd:
            for line in fd:
                line = line.strip()
                if self._check_line(line):
                    self._parse_line(line)

        # drop root node if only one callee
        if len(self._root) == 1:
            return self._root[0].make_self_root()
        return self._root

class _TraceTextParser(_TextParser):
    def __init__(self):
        self._text_lines = {}   # reset when parse?
        self._level = 0
        self._curr = self._root = None

    def _check_line(self, line:str):
        if line.count('|') != 1:
            return False
        if line.endswith('|'):
            return False
        return True

    def _is_call_line(self, line:str):
        if line.count('()') == 1:
            return True
        return False

    def _is_end_line(self, line:str):
        return line.endswith('}')

    @abstractmethod
    def _do_parse_dict(self, text_lines:dict) -> SNode:
        return self._root

    def do_parse_text(self, text:str) -> SNode:
        for line in text.split('\n'):
            line = line.strip()
            if self._check_line(line):
                self._parse_line(line)

        return self._do_parse_dict(self._text_lines)

    def do_parse_file(self, filename:str) -> SNode:
        with open(filename, 'r') as fd:
            for line in fd:
                line = line.strip()
                if self._check_line(line):
                    self._parse_line(line)

        return self._do_parse_dict(self._text_lines)

class FGraphTraceShowParser(_TraceTextParser):
    '''trace-cmd show'''

    def _parse_call_line(self, line:str):
        ss = line.split('|')
        cpu, duration = ss[0].split(' ', 1)
        cpu = int(cpu.strip(')'), 10)

        function = ss[1].lstrip()
        level = len(ss[1]) - len(function)
        return cpu, duration, function, level

    def _parse_end_line(self, line:str):
        ss = line.split("|")
        cpu, duration = ss[0].split(' ', 1)
        cpu = int(cpu.strip(')'), 10)
        level = len(ss[1]) - 1  # len('}')
        return cpu, duration, level

    def _parse_line(self, line:str):
        if self._is_call_line(line):
            cpu, duration, function, level = self._parse_call_line(line)
            item = ('call', cpu, duration, function, level)
            _mlog.debug(item)
            try:
                self._text_lines[cpu].append(item)
            except KeyError:
                self._text_lines[cpu] = [item,]
            return True

        if self._is_end_line(line):
            cpu, duration, level = self._parse_end_line(line)
            item = ('endcall', cpu, duration, level)
            _mlog.debug(item)
            try:
                self._text_lines[cpu].append(item)
            except KeyError:
                self._text_lines[cpu] = [item,]
            return True

        return False

    def _do_process_item(self, item:str):
        typename = item[0]
        if typename == 'call':
            cpu, duration, function, level = item[1:]
            _tlog.debug(f'{cpu} {duration} {function} {level} {self._level} {self._curr}')
            node = SNode(function, direction='>')

            if level == self._level:
                self._curr.parent.add_child(node)
            elif level > self._level:
                self._level = level
                self._curr.add_child(node)

            self._curr = node
            return True

        if typename == 'endcall':
            cpu, duration, level = item[1:]
            _tlog.debug(f"{level} {self._level} {self._curr}")
            if level < self._level:
                self._level = level
                self._curr = self._curr.parent
                # TODO: set duration of self._curr

            return True

        return False

    def _do_parse_items(self, items:list) -> SNode:
        for item in items:
            self._do_process_item(item)

        return self._root

    def _do_parse_dict(self, _dict) -> SNode:
        # notice: reset _root and _curr
        self._curr = self._root = SNode()

        # for each cpu
        for cpu, items in _dict.items():
            bak = self._curr

            self._level = 0
            # direction is always '>'
            node = SNode(f'cpu{cpu}', direction='>')
            self._curr = self._curr.add_child(node)
            self._do_parse_items(items)

            self._curr = bak

        return self._root


class FGraphTraceReportParser(_TraceTextParser):
    '''trace-cmd report'''

    def _parse_call_line(self, line:str):
        ss = line.split('|')
        tmp = ss[0]
        while tmp.count('  ') > 0:
            tmp = tmp.replace('  ', ' ')
        command, cpu, timestamp, action, duration = tmp.split(' ', 4)
        _tlog.debug(f"{tmp.split(' ', 4)} {cpu.strip('[]')}")
        cpu = int(cpu.strip('[]'), 10)

        function = ss[1].lstrip()
        level = len(ss[1]) - len(function)
        return command, cpu, timestamp, action, duration, function, level

    def _parse_end_line(self, line:str):
        ss = line.split("|")
        tmp = ss[0]
        while tmp.count('  ') > 0:
            tmp = tmp.replace('  ', ' ')
        command, cpu, timestamp, action, duration = tmp.split(' ', 4)
        cpu = int(cpu.strip('[]'), 10)
        level = len(ss[1]) - 1  # len('}')
        return command, cpu, timestamp, action, duration, level

    def _parse_line(self, line:str):
        if self._is_call_line(line):
            command, cpu, timestamp, action, duration, function, level = self._parse_call_line(line)
            item = ('call', command, cpu, timestamp, action, duration, function, level)
            _mlog.debug(item)
            try:
                self._text_lines[command].append(item)
            except KeyError:
                self._text_lines[command] = [item,]
            return True

        if self._is_end_line(line):
            command, cpu, timestamp, action, duration, level = self._parse_end_line(line)
            item = ('endcall', command, cpu, timestamp, action, duration, level)
            _mlog.debug(item)
            try:
                self._text_lines[command].append(item)
            except KeyError:
                self._text_lines[command] = [item,]
            return True

        return False

    def _do_process_item(self, item:str):
        typename = item[0]
        if typename == 'call':
            command, cpu, timestamp, action, duration, function, level = item[1:]
            _tlog.debug(f"{cpu} {duration} {function} {level} {self._level} {self._curr}")
            node = SNode(function, direction='>')

            if level == self._level:
                self._curr.parent.add_child(node)
            elif level > self._level:
                self._level = level
                self._curr.add_child(node)

            self._curr = node
            return True

        if typename == 'endcall':
            command, cpu, timestamp, action, duration, level = item[1:]
            _tlog.debug(f"{level} {self._level} {self._curr}")
            if level < self._level:
                self._level = level
                self._curr = self._curr.parent
                # TODO: set duration of self._curr

            return True

        return False

    def _do_parse_items(self, items:list) -> SNode:
        for item in items:
            self._do_process_item(item)

        return self._root

    def _do_parse_dict(self, _dict) -> SNode:
        # notice: reset _root and _curr
        self._curr = self._root = SNode()

        # for each command
        for command, items in _dict.items():
            bak = self._curr

            self._level = 0
            node = SNode(f'{command}', direction='>')
            self._curr = self._curr.add_child(node)
            self._do_parse_items(items)

            self._curr = bak

        return self._root
